/* This file is part of swapper project
 *
 * Copyright (C) 2020 The Swapper Project Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.swapper.mock;

import com.swapper.mock.supports.*;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;

public final class MockContext {
  private final Random random;
  private final List<MockerFactory> factories;
  private final Map<Type, Mocker<?>> mockerCache = new HashMap<>();
  private final Map<Class<?>, MockConfig> configCache = new HashMap<>();

  public MockContext() {
    this(new Random(System.currentTimeMillis()));
  }

  public MockContext(Random random) {
    this(random, Collections.emptyList());
  }

  public MockContext(Random random, Collection<MockerFactory> factories) {
    this.random = Objects.requireNonNull(random);
    List<MockerFactory> supports = new ArrayList<>();
    supports.add(BooleanMockerFactory.INSTANCE);
    supports.add(CharacterMockerFactory.INSTANCE);
    supports.add(ByteMockerFactory.INSTANCE);
    supports.add(ShortMockerFactory.INSTANCE);
    supports.add(IntegerMockerFactory.INSTANCE);
    supports.add(LongMockerFactory.INSTANCE);
    supports.add(FloatMockerFactory.INSTANCE);
    supports.add(DoubleMockerFactory.INSTANCE);
    supports.add(BigIntegerMockerFactory.INSTANCE);
    supports.add(BigDecimalMockerFactory.INSTANCE);
    supports.add(NumberMockerFactory.INSTANCE);
    supports.add(StringMockerFactory.INSTANCE);
    supports.add(EnumMockerFactory.INSTANCE);
    supports.add(ArrayMockerFactory.INSTANCE);
    supports.add(CollectionMockerFactory.INSTANCE);
    supports.add(MapMockerFactory.INSTANCE);
    supports.add(ObjectMockerFactory.INSTANCE);
    supports.addAll(factories);
    supports.add(ReflectionMockerFactory.INSTANCE);
    this.factories = Collections.unmodifiableList(supports);
  }

  private static final class DefaultMockContextHolder {
    private static final MockContext defaultMockContext = new MockContext();
  }

  public static <T> T defaultMock(Type type) {
    return DefaultMockContextHolder.defaultMockContext.mock(type);
  }

  public static <T> T defaultMock(Class<T> type) {
    return DefaultMockContextHolder.defaultMockContext.mock(type);
  }

  @SuppressWarnings("unchecked")
  public <T> T mock(Type type) {
    return (T) getMocker(type).mock(this);
  }

  public <T> T mock(Class<T> type) {
    return mock((Type) type);
  }

  public Random random() {
    return random;
  }

  @SuppressWarnings("unchecked")
  private <T> Mocker<T> getMocker(Type type) {
    Objects.requireNonNull(type);
    Mocker<?> mocker = mockerCache.get(type);
    if (mocker == null) {
      for (MockerFactory factory : factories) {
        if ((mocker = factory.create(type)) != null) {
          mockerCache.put(type, mocker);
          return (Mocker<T>) mocker;
        }
      }
      throw new IllegalStateException("A type not supported by the current version:" + type);
    }
    return (Mocker<T>) mocker;
  }

  @SuppressWarnings("unchecked")
  public <T extends MockConfig> T getConfig(Class<T> type) {
    Objects.requireNonNull(type);
    T config = (T) configCache.get(type);
    if (config == null) {
      try {
        config = type.getDeclaredConstructor().newInstance();
        configCache.put(type, config);
        return config;
      } catch (NoSuchMethodException e) {
        throw new IllegalStateException(type.getSimpleName() + " requires a no-argument constructor.", e);
      } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
        throw new IllegalStateException(e);
      }
    }
    return config;
  }

  public <T extends MockConfig> MockContext withConfig(Class<T> type, Consumer<T> consumer) {
    consumer.accept(getConfig(type));
    return this;
  }
}
